﻿#region License
/* ---------------------------------------------------------------- *
 *
 * This file is part of the Xcoordination Application Space
 * ("XcoAppSpace") http://www.xcoordination.com
 *
 * Copyright (C) 2009 Xcoordination http://www.xcoordination.com
 *
 * XcoAppSpace is free software; you can redistribute it and/or
 * modify it under the terms of version 2 of the GNU General
 * Public License as published by the Free Software Foundation.
 *
 * XcoAppSpace is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see http://www.gnu.org/licenses/
 * or write to the Free Software Foundation, Inc., 51 Franklin
 * Street, Fifth Floor, Boston, MA 02110-1301, USA.
 *
 * ---------------------------------------------------------------- */
#endregion

using System;
using System.Collections.Generic;
using System.Text;
using XcoAppSpaces.Contracts.Exceptions;
using XcoAppSpaces.Contracts.Transport;
using System.Net;
using System.Threading;
using System.Linq;
using XcoAppSpaces.Contracts.Logging;
using System.Net.Sockets;
using System.IO;
using XcoAppSpaces.Transport.Sockets.Auth.Contract;
using XcoAppSpaces.Transport.Sockets.Auth;

namespace XcoAppSpaces.Transport.Sockets
{
	/// <summary>
	/// Communication Service that send/receives messages to/from remote spaces via TCP sockets.
	/// </summary>
	public class XcoTCPTransportService : IXcoTransportService
	{
		class CurrentlyConnectingSocket
		{
			public string Address { get; set; }
			public List<XcoMessage> WaitingMessages { get; set; }
		}

		/// <summary>
		/// Event that is raised when messages are received.
		/// </summary>
		public event MessageReceivedDelegate OnMessageReceived;

		private readonly string hostAddress = "SL-" + Guid.NewGuid(); //since there is no host address in a silverlight application, fake one
		bool running = false;

		private readonly IXcoLogger log = XcoLoggerFactory.CreateInstance(typeof(XcoTCPTransportService));
		private readonly Dictionary<string, Connection> activeConnections = new Dictionary<string, Connection>();
		private readonly List<CurrentlyConnectingSocket> currentlyConnecting = new List<CurrentlyConnectingSocket>();
		private object senderCreationLockObj = new object();

		internal static int timeoutCheckInterval = 30000; //check for connection timeouts every 30 seconds
		private TimeSpan connectionLifetime = TimeSpan.FromSeconds(60); //default is 60 seconds connection timeout
		private Timer timeoutCheckTimer; //the timer that triggers the timeout checks

		/// <summary>
		/// Security Settings that are used when establishing a connection
		/// </summary>
		public SecuritySettings SecuritySettings { get; private set; }
		private DoClientHandshake clientHandshake;

		/// <summary>
		/// Creates a new XcoTCPTransportService.
		/// </summary>
		public XcoTCPTransportService()
		{
			SecuritySettings = new SecuritySettings { AuthenticationKey = null };

			clientHandshake = DoClientHandshake.CreateDefaultInstance();
			clientHandshake.Out_AuthenticatedConnection += c => AddToActiveConnectionsAndStartReceiveMessages(c.RemoteAddress, c.Socket);
			clientHandshake.Out_InvalidConnection += c => CloseNotAuthenticatedConnection(c.RemoteAddress, c.Socket);
		}

		/// <summary>
		/// The Address of the server.
		/// </summary>
		public string LocalAddress
		{
			get { return hostAddress; }
		}

		/// <summary>
		/// The time until a connect is closed when it has not been used (meaning not messages have been sent or received).
		/// </summary>
		public TimeSpan ConnectionLifetime
		{
			get { return connectionLifetime; }
			set { connectionLifetime = value; }
		}

		/// <summary>
		/// Returns a list of addresses to which there are currently active tcp connections.
		/// </summary>
		public IEnumerable<string> ActiveConnections
		{
			get
			{
				IEnumerable<string> conns;
				lock (activeConnections)
				{
					conns = activeConnections.Keys.ToList();
				}
				return conns;
			}
		}

		/// <summary>
		/// Initializes the transport service.
		/// </summary>
		/// <param name="serviceRegistry">Service registry with other services of the space instance.</param>
		public void Initialize(XcoAppSpaces.Contracts.Service.IXcoServiceRegistry serviceRegistry)
		{
		}

		/// <summary>
		/// Starts the communication service.
		/// </summary>
		public void Start()
		{
			if (!running)
			{
				log.Info("Starting XcoTCPTransportService with client id " + hostAddress);
				running = true;
				timeoutCheckTimer = new Timer(_ => CheckConnectionTimeouts(), null, timeoutCheckInterval, timeoutCheckInterval);
				clientHandshake.Inject(SecuritySettings);
			}
		}

		/// <summary>
		/// Start priority of the service.
		/// </summary>
		public int StartPriority
		{
			get { return 5; }
		}

		/// <summary>
		/// Stops the service and closes all open connections.
		/// </summary>
		public void Stop()
		{
			if (running)
			{
				running = false;
				timeoutCheckTimer.Dispose();
				ReleaseConnections();
			}
		}
		
		/// <summary>
		/// Releases all currently active connections.
		/// </summary>
		internal void ReleaseConnections()
		{
			lock (activeConnections)
			{
				foreach (var client in activeConnections.Values)
				{
					try
					{
						client.Socket.Close();
					}
					catch { }
				}
				activeConnections.Clear();
			}
		}

		/// <summary>
		/// Sends a message to a remote space.
		/// </summary>
		/// <param name="msg">The message that should be sent.</param>
		/// <param name="remoteAddress">The address where the message should be sent to.</param>
		public void Send(XcoMessage msg, string remoteAddress)
		{
			log.Debug("Sending message to: " + remoteAddress);
			if (!running)
				throw new XcoCommunicationException("Server for remote communication has not been started!");

			var conn = GetConnection(remoteAddress);
			if (conn != null)
				SendMessage(conn, msg, false);
			else
				CreateConnectionAndSendMessage(remoteAddress, msg);
		}

		private Connection GetConnection(string remoteAddress)
		{
			lock (activeConnections)
			{
				Connection conn;
				return activeConnections.TryGetValue(remoteAddress, out conn) ? conn : null;
			}
		}

		private void CloseConnection(string remoteAddress)
		{
			lock (activeConnections)
			{
				var conn = GetConnection(remoteAddress);
				CloseConnection(conn);
			}
		}

		private void CloseConnection(Connection conn)
		{
			if (conn != null)
			{
				lock (activeConnections)
				{

					activeConnections.Remove(conn.RemoteAddress);
				}
				try
				{
					conn.Socket.Close();
				}
				catch { }
			}
		}

		private void SendMessage(Connection conn, XcoMessage msg, bool isNewConnection)
		{
			conn.LastSendTime = DateTime.Now;
			var msgBytes = conn.MsgConverter.Convert(msg, hostAddress);
			conn.Socket.Send(msgBytes, (s, e) => HandleSendMessageCompleted(e, conn.RemoteAddress, msg, isNewConnection));
		}

		private void HandleSendMessageCompleted(SocketAsyncEventArgs e, string remoteAddress, XcoMessage msg, bool isNewConnection)
		{
			if (e.SocketError != SocketError.Success)
			{
				log.Error("TCP send error: " + e.SocketError);
				if (!isNewConnection) //if connection is not new, try to reconnect
				{
					log.Error("Trying to create new connection...");
					CloseConnection(remoteAddress);
					CreateConnectionAndSendMessage(remoteAddress, msg);
				}
				else
				{
					msg.RaiseTransferFailedEvent(new XcoCommunicationException("Error sending Message: " + e.SocketError, e.ConnectByNameError));
				}
			}
			else
			{
				msg.RaiseTransferSuccessfulEvent();
			}
		}

		private void CreateConnectionAndSendMessage(string remoteAddress, XcoMessage msg)
		{
			lock (activeConnections)
			{
				lock (currentlyConnecting)
				{
					//make sure the connection has not been created at this moment
					var conn = GetConnection(remoteAddress);
					if (conn != null)
					{
						SendMessage(conn, msg, false);
						return;
					}

					//check if a connection is currently being established to this socket - if yes, just add the message to the ones that are waiting for this connection
					var connecting = currentlyConnecting.FirstOrDefault(c => c.Address == remoteAddress);
					if (connecting != null)
					{
						log.Debug("Connection to " + remoteAddress + " is already being established - waiting with sending until connected.");
						connecting.WaitingMessages.Add(msg);
						return;
					}

					//if not: add info about the connecting being created, and create it
					currentlyConnecting.Add(new CurrentlyConnectingSocket
					{
						Address = remoteAddress,
						WaitingMessages = new List<XcoMessage>{msg}
					});
				}
			}

			CreateConnection(remoteAddress);
		}

		private CurrentlyConnectingSocket GetAndRemoveFromCurrentlyConnecting(string remoteAddress)
		{
			lock (activeConnections)
			{
				lock (currentlyConnecting)
				{
					var connecting = currentlyConnecting.FirstOrDefault(c => c.Address == remoteAddress);
					currentlyConnecting.Remove(connecting);
					return connecting;
				}
			}
		}

		private void CreateConnection(string remoteAddress)
		{
			log.Debug("Creating connection to: " + remoteAddress);
			var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
			socket.Connect(remoteAddress, (s, e) => HandleCreateConnectionCompleted(e, socket, remoteAddress));
		}

		private void HandleCreateConnectionCompleted(SocketAsyncEventArgs e, Socket socket, string remoteAddress)
		{
			if (e.SocketError == SocketError.Success)
			{

				if (SecuritySettings.AuthenticationKey == null)
					AddToActiveConnectionsAndStartReceiveMessages(remoteAddress, socket);
				else //start handshake if a key has been defined
					clientHandshake.In_Connection(new TcpClient { RemoteAddress = remoteAddress, Socket = socket });
			}
			else
			{
				var ex = new XcoCommunicationException("Connection Error: " + e.SocketError, e.ConnectByNameError);
				ConnectionErrorMarkAllWaitingMessagesAsFailed(remoteAddress, ex);
			}
		}

		private void CloseNotAuthenticatedConnection(string remoteAddress, Socket socket)
		{
			try {
				socket.Close();
			} catch { }
			ConnectionErrorMarkAllWaitingMessagesAsFailed(remoteAddress, new XcoCommunicationException("TCP Connection error: Handshake for authentication failed!"));
		}

		private void AddToActiveConnectionsAndStartReceiveMessages(string remoteAddress, Socket socket)
		{
			var conn = new Connection { RemoteAddress = remoteAddress, Socket = socket, LastSendTime = DateTime.Now };
			lock (activeConnections)
			{
				activeConnections.Add(conn.RemoteAddress, conn);
				ConnectionEstablishedSendAllWaitingMessages(conn.RemoteAddress, conn);
			}
			StartReceiveMessages(conn);
		}

		private void ConnectionErrorMarkAllWaitingMessagesAsFailed(string remoteAddress, XcoCommunicationException ex)
		{
			var connecting = GetAndRemoveFromCurrentlyConnecting(remoteAddress);
			if (connecting != null)
			{
				log.Error("TCP Connection error (" + connecting.WaitingMessages.Count + " messages should have been sent to " + remoteAddress + "): " + ex);
				foreach (var m in connecting.WaitingMessages)
				{
					m.RaiseTransferFailedEvent(ex);
				}
			}
			else
				log.Error("TCP Connection error (" + remoteAddress + " - no waiting messages??): " + ex);
		}

		private void ConnectionEstablishedSendAllWaitingMessages(string remoteAddress, Connection conn)
		{
			var connecting = GetAndRemoveFromCurrentlyConnecting(remoteAddress);
			if (connecting != null)
			{
				log.Debug("Connection to " + remoteAddress + " established - sending " + connecting.WaitingMessages.Count + " messages.");
				foreach (var m in connecting.WaitingMessages)
				{
					SendMessage(conn, m, true);
				}
			}
			else
				log.Warn("Warn: Connection to " + remoteAddress + " established but no messages waiting to be sent??");
		}

		private void StartReceiveMessages(Connection conn)
		{
			try
			{
				var evt = new SocketAsyncEventArgs();
				evt.Completed += (s, e) => HandleReceivedData(conn, e);
				var transferBuffer = new byte[conn.Socket.ReceiveBufferSize];
				evt.SetBuffer(transferBuffer, 0, transferBuffer.Length);

				conn.Socket.ReceiveAsync(evt);
			}
			catch (Exception ex)
			{
				log.Error("Socket receive error - closing connection: " + ex.Message);
				CloseConnection(conn);
			}
		}

		private void HandleReceivedData(Connection conn, SocketAsyncEventArgs e)
		{
			if (e.SocketError != SocketError.Success)
			{
				log.Error("Socket receive error: " + e.SocketError);
				
				if (!conn.Socket.Connected)
					return;

				if (running)
				{
					StartReceiveMessages(conn);
					return;
				}
			}

			if (e.BytesTransferred == 0) //when 0 bytes have been received it means that the connection has been closed on the other side
				CloseConnection(conn);

			//log.Debug("Msg received (" + e.BytesTransferred + " bytes)");
			conn.LastSendTime = DateTime.Now;
			
			var data = e.Buffer.SubArray(0, e.BytesTransferred);
			string remoteAddress;
			var messages = conn.MsgConverter.Convert(data, out remoteAddress, 
				//() => { }, 
				ex => 
				{
					log.Error("Conversion error for received message: " + ex);
				});

			foreach (var msg in messages) //msg is null e.g. when the received data was still incomplete
				OnMessageReceived(msg, remoteAddress, this);
			//else
			//	log.Error("Conversion error for received message! (msg is null)");

			//receive next message
			if (running)
				StartReceiveMessages(conn);
		}


		/// <summary>
		/// Checks if any connections have run into a timeout (not sent or received any message for a 
		/// defined amount of time), and removes and closes these connections.
		/// </summary>
		private void CheckConnectionTimeouts()
		{
			DateTime checkDate = DateTime.Now.Subtract(connectionLifetime);

			var timedOut = new List<Connection>();
			var timedOutKeys = new List<string>();
			lock (activeConnections)
			{
				foreach (var address in activeConnections.Keys)
				{
					var conn = activeConnections[address];
					if (conn.LastSendTime <= checkDate)
					{
						timedOut.Add(conn);
						timedOutKeys.Add(address);
						log.Debug("Connection timed out for remote space: " + address);
					}
				}
				//remove timed out connections from the list
				foreach (var key in timedOutKeys)
				{
					activeConnections.Remove(key);
				}
			}
			//disconnect sockets that have timed out
			foreach (var conn in timedOut)
			{
				try
				{
					conn.Socket.Close();
				}
				catch { }
			}
		}

		#region IDisposable Members

		/// <summary>
		/// Stops the service and closes all open connections.
		/// </summary>
		public void Dispose()
		{
			this.Stop();
		}

		#endregion
	}
}
